#include <Render/Include/RendererDX9.h>

#include <Render/Include/RenderObjectDX9.h>

#include <D3dx9shape.h>

using namespace Render;

// ========== LIFECYCLE ========== //

RendererDX9::RendererDX9(IWinWindow* window)
    :Renderer()
{
    LPDIRECT3D9 p_d3d9;
    D3DDISPLAYMODE d3ddm;
    
    // Create the D3D object.
    if (NULL == (p_d3d9 = Direct3DCreate9(D3D_SDK_VERSION)))
    {
        throw -1;
    }

    // Get the current desktop display mode, so we can set up a back
    // buffer of the same format
    if (FAILED(p_d3d9->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm)))
    {
        throw -1;
    }

    D3DPRESENT_PARAMETERS d3dpp; 
    ZeroMemory(&d3dpp, sizeof(d3dpp));

    d3dpp.Windowed = TRUE;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    // We're using the current display mode. 
    d3dpp.BackBufferFormat = d3ddm.Format;
    d3dpp.BackBufferWidth = window->getWidth();
    d3dpp.BackBufferHeight = window->getHeight();

    d3dpp.EnableAutoDepthStencil = TRUE;
    d3dpp.AutoDepthStencilFormat = D3DFMT_D32;
    HRESULT hr = p_d3d9->CheckDepthStencilMatch(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, d3ddm.Format, d3ddm.Format, D3DFMT_D32);

    if (FAILED (hr))
    {
        d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
    }

    // Create the D3DDevice
    if (FAILED(p_d3d9->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, window->getHWND(), D3DCREATE_HARDWARE_VERTEXPROCESSING, &d3dpp, &(this->p_device))))
    {
        throw -1;
    }

    /// Creates and sets the viewport
    D3DVIEWPORT9 vp;
    vp.Width = window->getWidth();
    vp.Height = window->getHeight();
    vp.MinZ = 0.0f;
    vp.MaxZ = 1.0f;
    vp.X = 0;
    vp.Y = 0;
    this->p_device->SetViewport(&vp);

    // Turn on the zbuffer
    this->p_device->SetRenderState(D3DRS_ZENABLE, TRUE);

    /// creates the default objects used for rendering
    this->createSphereRenderObject();
    this->createCubeRenderObject();

    // to render text
    // Create the D3DX Font
    hr = D3DXCreateFont( this->p_device, 20, 0, FW_BOLD, 1, FALSE, DEFAULT_CHARSET,
        OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE,
        L"Arial", &this->font ) ;

}

RendererDX9::~RendererDX9(void)
{
    delete this->sphere_render_object;
    delete this->cube_render_object;
    this->font->Release();
    this->p_device->Release();
}

// ========== OVERLODED OPERATORS ========== //

// ========== METHODS ========== //

void RendererDX9::renderScene(Model::Scene* scene)
{
    HRESULT hr;
    
    hr = this->p_device->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, (D3DCOLOR)0x00000000, 1.0f, 0x00000000);
    if (D3D_OK == this->p_device->BeginScene()) 
    {
        this->configViewPoint(scene);

        this->renderSceneWalls(scene);

        const Model::Snake* snake = scene->getSnake();

        const Model::Element* head_element = snake->getHeadElement();
        this->renderSnakeHead(head_element);

        const Model::Element* body_element = head_element->getNextElement();
        while (NULL != body_element)
        {
            this->renderSnakeBodyElement(body_element);
            body_element = body_element->getNextElement();
        }
        
        const Model::Element* free_element = scene->getFreeElement();
        this->renderSnakeBodyElement(free_element);
        
        this->p_device->EndScene();
    }
    this->drawText();
    this->p_device->Present(NULL, NULL, NULL, NULL);
    this->clearText();
}

void RendererDX9::renderBlack()
{
    HRESULT hr;
    hr = this->p_device->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, (D3DCOLOR)0x00000000, 1.0f, 0x00000000);
    this->drawText();
    this->p_device->Present(NULL, NULL, NULL, NULL);
    this->clearText();
}

// ========== GETTERS ========== //

// ========== SETTERS ========== //

// ========== INQUIRY ========== //

// ========== INTERNAL METHODS ========== //

void RendererDX9::createSphereRenderObject()
{
    HRESULT hr;
    LPD3DXMESH mesh_ball;
    LPD3DXBUFFER adjacency_mesh_ball;
    hr = D3DXCreateSphere( this->p_device, 1.0, 20, 20, &mesh_ball, &adjacency_mesh_ball);
    this->sphere_render_object = new RenderObjectDX9(mesh_ball, adjacency_mesh_ball);        
}

void RendererDX9::createCubeRenderObject()
{
    HRESULT hr;
    LPD3DXMESH mesh_ball;
    LPD3DXBUFFER adjacency_mesh_ball;
    hr = D3DXCreateBox( this->p_device, 1.0, 1.0, 1.0, &mesh_ball, &adjacency_mesh_ball);
    this->cube_render_object = new RenderObjectDX9(mesh_ball, adjacency_mesh_ball);        
}

void RendererDX9::configViewPoint(Model::Scene* scene)
{
    /// set the viewpoint
    D3DXMATRIX view_matrix;
    D3DXVECTOR3 camera_position;
    D3DXVECTOR3 camera_at;
    D3DXVECTOR3 camera_up;
    if (this->render_3D)
    {
        const Model::Snake* snake = scene->getSnake();
        const Model::Element* head_element = snake->getHeadElement();
        int at_x = 0;
        int at_y = 0;
        snake->getDirection(&at_x, &at_y);
        camera_position = D3DXVECTOR3((float)(head_element->getXPosition() - at_x * 5), (float)(head_element->getYPosition() - at_y * 5), 5.0f);
        camera_at = D3DXVECTOR3((float)(head_element->getXPosition() + at_x), (float)(head_element->getYPosition() + at_y), 0.0f);
        camera_up = D3DXVECTOR3(0.0f, 0.0f, 1.0f);
    }
    else
    {
        camera_position = D3DXVECTOR3((float)scene->getWidth()/2.0f, (float)scene->getHeight() / 2.0f, 20.0f); 
        camera_at = D3DXVECTOR3((float)scene->getWidth()/2.0f, (float)scene->getHeight() / 2.0f, 0.0f); 
        camera_up = D3DXVECTOR3(0.0f, 1.0f, 0.0f);
    }
    
    D3DXMatrixLookAtLH(&view_matrix, &camera_position, &camera_at, &camera_up);
    this->p_device->SetTransform(D3DTS_VIEW, &view_matrix);

    /// set the projection
    D3DXMATRIX projection_matrix;
    if (this->render_3D)
    {
        D3DXMatrixPerspectiveFovLH(&projection_matrix, 3.14f/2.0f, 1.0f, 0.6f, 1000.0f);
    }
    else
    {
        D3DXMatrixOrthoLH(&projection_matrix, (float)scene->getWidth() + 5.0f, (float)scene->getHeight() + 5.0f, 1.0f, 1000.0f);
    }
    
    this->p_device->SetTransform(D3DTS_PROJECTION, &projection_matrix);
}

void RendererDX9::renderSceneWalls(Model::Scene* scene)
{
    this->p_device->SetRenderState(D3DRS_FILLMODE , D3DFILLMODE::D3DFILL_SOLID);

    this->p_device->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);

    this->p_device->SetRenderState(D3DRS_EMISSIVEMATERIALSOURCE, D3DMCS_MATERIAL);

    D3DMATERIAL9 d3d9material;
    ZeroMemory(&d3d9material, sizeof(D3DMATERIAL9));
    d3d9material.Emissive.r = 1.0f;
    d3d9material.Emissive.g = 1.0f;
    d3d9material.Emissive.b = 1.0f;
    d3d9material.Emissive.a = 1.0f;

    this->p_device->SetMaterial(&d3d9material);

    this->p_device->SetTexture (0, NULL);

    LPD3DXMESH d3dxmesh = reinterpret_cast<RenderObjectDX9*>(this->cube_render_object)->getLPD3DXMESH();
    
    /// set the transformation matrix for the walls
    D3DXMATRIX scale_matrix;
    D3DXMATRIX position_matrix;
    D3DXMATRIX world_matrix;
    //vertical walls
    D3DXMatrixScaling(&scale_matrix, 1.0f, (float)scene->getHeight() + 2.0f, 1.0f);
    D3DXMatrixTranslation(&position_matrix, -1.0f, (float)scene->getHeight() / 2.0f - 0.5f, 0.0f);
    D3DXMatrixMultiply(&world_matrix, &scale_matrix, &position_matrix);
    this->p_device->SetTransform(D3DTS_WORLDMATRIX(0), &world_matrix);
    if (d3dxmesh != NULL)
    {
        HRESULT hr = d3dxmesh->DrawSubset(0);
    }

    D3DXMatrixTranslation(&position_matrix, (float)scene->getWidth(), (float)scene->getHeight() / 2.0f - 0.5f, 0.0f);
    D3DXMatrixMultiply(&world_matrix, &scale_matrix, &position_matrix);
    this->p_device->SetTransform(D3DTS_WORLDMATRIX(0), &world_matrix);
    d3dxmesh->DrawSubset(0);

    //horizontal walls
    D3DXMatrixScaling(&scale_matrix, (float)scene->getWidth()  + 2.0f, 1.0f, 1.0f);
    D3DXMatrixTranslation(&position_matrix, (float)scene->getWidth() / 2.0f - 0.5f, -1.0f, 0.0f);
    D3DXMatrixMultiply(&world_matrix, &scale_matrix, &position_matrix);
    this->p_device->SetTransform(D3DTS_WORLDMATRIX(0), &world_matrix);
    d3dxmesh->DrawSubset(0);

    D3DXMatrixTranslation(&position_matrix, (float)scene->getWidth() / 2.0f - 0.5f, (float)scene->getHeight(), 0.0f);
    D3DXMatrixMultiply(&world_matrix, &scale_matrix, &position_matrix);
    this->p_device->SetTransform(D3DTS_WORLDMATRIX(0), &world_matrix);
    d3dxmesh->DrawSubset(0);

}

void RendererDX9::renderSnakeHead(const Model::Element* &element)
{
    /// set the transformation matrix for the element
    D3DXMATRIX scale_matrix;
    D3DXMatrixScaling(&scale_matrix, 0.45f, 0.45f, 0.45f);

    D3DXMATRIX position_matrix;
    D3DXMatrixTranslation(&position_matrix, element->getXPosition(), element->getYPosition(), 0.0f);

    D3DXMATRIX world_matrix;
    D3DXMatrixMultiply(&world_matrix, &scale_matrix, &position_matrix);
    this->p_device->SetTransform(D3DTS_WORLDMATRIX(0), &world_matrix);

    this->p_device->SetRenderState(D3DRS_FILLMODE , D3DFILLMODE::D3DFILL_SOLID);

    this->p_device->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);

    this->p_device->SetRenderState(D3DRS_EMISSIVEMATERIALSOURCE, D3DMCS_MATERIAL);

    D3DMATERIAL9 d3d9material;
    ZeroMemory(&d3d9material, sizeof(D3DMATERIAL9));
    d3d9material.Emissive.r = 0.0f;
    d3d9material.Emissive.g = 1.0f;
    d3d9material.Emissive.b = 0.0f;
    d3d9material.Emissive.a = 1.0f;

    this->p_device->SetMaterial(&d3d9material);

    this->p_device->SetTexture (0, NULL);

    LPD3DXMESH d3dxmesh = reinterpret_cast<RenderObjectDX9*>(this->sphere_render_object)->getLPD3DXMESH();
    if (d3dxmesh != NULL)
    {
        HRESULT hr = d3dxmesh->DrawSubset(0);
    }
}

void RendererDX9::renderSnakeBodyElement(const Model::Element* &element)
{
    /// set the transformation matrix for the element
    D3DXMATRIX scale_matrix;
    D3DXMatrixScaling(&scale_matrix, 0.9f, 0.9f, 0.9f);

    D3DXMATRIX position_matrix;
    D3DXMatrixTranslation(&position_matrix, element->getXPosition(), element->getYPosition(), 0.0f);

    D3DXMATRIX world_matrix;
    D3DXMatrixMultiply(&world_matrix, &scale_matrix, &position_matrix);
    this->p_device->SetTransform(D3DTS_WORLDMATRIX(0), &world_matrix);

    this->p_device->SetRenderState(D3DRS_FILLMODE , D3DFILLMODE::D3DFILL_SOLID);

    this->p_device->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);

    this->p_device->SetRenderState(D3DRS_EMISSIVEMATERIALSOURCE, D3DMCS_MATERIAL);

    D3DMATERIAL9 d3d9material;
    ZeroMemory(&d3d9material, sizeof(D3DMATERIAL9));
    d3d9material.Emissive.r = 0.0f;
    d3d9material.Emissive.g = 0.0f;
    d3d9material.Emissive.b = 1.0f;
    d3d9material.Emissive.a = 1.0f;

    this->p_device->SetMaterial(&d3d9material);

    this->p_device->SetTexture (0, NULL);

    LPD3DXMESH d3dxmesh = reinterpret_cast<RenderObjectDX9*>(this->cube_render_object)->getLPD3DXMESH();
    if (d3dxmesh != NULL)
    {
        HRESULT hr = d3dxmesh->DrawSubset(0);
    }
}

void RendererDX9::drawText()
{
    HRESULT hr;
    RECT rc;    
    for (unsigned int i = 0; i < this->text_to_render.size(); i++)
    {
        SetRect(&rc, this->text_to_render[i]->pos_x, this->text_to_render[i]->pos_y, 200, 200 );
        hr = font->DrawTextA(NULL, this->text_to_render[i]->text.c_str(), -1, &rc, DT_NOCLIP, D3DCOLOR_RGBA(255, 255, 255, 255));         
    }
}
